﻿namespace PugLib.Extensions.Collections
{
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Linq;
	using System.Threading.Tasks;

	public static class PugExtensions
	{
		public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> list)
		{
			return list ?? Enumerable.Empty<T>();
		}

		public static IEnumerable<TResult> Map<TArg, TResult>(this IEnumerable<TArg> list, Func<TArg, TResult> func)
		{
			foreach (TArg item in list)
			{
				yield return func(item);
			}
		}

		public static TValue MapOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
		{
			TValue result;
			return dictionary.TryGetValue(key, out result) ? result : default(TValue);
		}


		public static IEnumerable<TOutput> MapAll<TInput, TOutput>(this Func<TInput, TOutput> mapper,
		                                                           IEnumerable<TInput> input)
		{
			return input.Select(mapper);
		}


		public static IEnumerable<U> SelectMany<T, U>(this IEnumerable<T> m, Func<T, IEnumerable<U>> k)
		{
			foreach (T x in m)
			{
				foreach (U y in k(x))
				{
					yield return y;
				}
			}
		}

		public static string EnumerableToString<T>(this IEnumerable<T> enumerable)
		{
			return StringFormatter.FormatData(", ", enumerable);
		}

		public static TValue GetOrCreate<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
			where TValue : new()
		{
			TValue ret;
			if (!dictionary.TryGetValue(key, out ret))
			{
				ret = new TValue();
				dictionary[key] = ret;
			}
			return ret;
		}

		public static Dictionary<TKey, TValue> FilterBy<TKey, TValue>(this Dictionary<TKey, TValue> items,
		                                                              Func<TKey, TValue, bool> filter)
		{
			Dictionary<TKey, TValue> result = new Dictionary<TKey, TValue>();
			items.ForEach(element =>
			              {
			              	if (filter(element.Key, element.Value))
			              	{
			              		result.Add(element.Key, element.Value);
			              	}
			              });
			return result;
		}

		/// <summary>
		/// What this method does is if two adjacent elements match (as defined by the match delegate returning true) 
		/// they will be reduced into one element by calling the reduce delegate. For example, 
		/// the Sum<T>()standard extension method could be implemented using Reduce<T>() as, 
		/// var sum = numbers.Reduce((a, b) => true, (a, b) => a + b).First();
		/// 
		/// http://www.removingalldoubt.com/permalink.aspx/6080a8a8-bb63-4492-acd2-1398f086fca0
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="e"></param>
		/// <param name="matchOperation"></param>
		/// <param name="reduceOperation"></param>
		/// <returns></returns>
		public static IEnumerable<T> Reduce<T>(this IEnumerable<T> e,
		                                       Func<T, T, bool> matchOperation,
		                                       Func<T, T, T> reduceOperation)
		{
			IEnumerator<T> en = e.GetEnumerator();
			if (en.MoveNext())
			{
				T last = en.Current;
				while (en.MoveNext())
				{
					if (!matchOperation(last, en.Current))
					{
						yield return last;
						last = en.Current;
					}
					else
					{
						last = reduceOperation(last, en.Current);
					}
				}
				yield return last;
			}
		}


		public static IEnumerable<TResult> Repeat<TArg, TResult>
			(this TArg val, Func<TArg, TResult> func, int times)
		{
			for (int i = 0; i < times; i++)
			{
				yield return func(val);
			}
		}

		public static T Fold<T>(
			this IEnumerable<T> list,
			Func<T, T, T> func)
		{
			return Fold(list, func, default(T));
		}

		public static TResult Fold<TArg, TResult>(
			this IEnumerable<TArg> list,
			Func<TResult, TArg, TResult> func)
		{
			return Fold(list, func, default(TResult));
		}

		public static TResult Fold<TArg, TResult>(
			this IEnumerable<TArg> list,
			Func<TResult, TArg, TResult> func, TResult result)
		{
			foreach (TArg item in list)
			{
				result = func(result, item);
			}
			return result;
		}

		public static IEnumerable<T> Apply<T>(this IEnumerable<T> list)
		{
			List<T> result = new List<T>();
			foreach (T item in list)
			{
				result.Add(item);
			}
			return result;
		}

		public static IEnumerable<T> Filter<T>(
			this IEnumerable<T> list,
			Predicate<T> func)
		{
			foreach (T item in list)
			{
				if (func(item))
				{
					yield return item;
				}
			}
		}

		public static IEnumerable<T[]> Zip<T>(this IEnumerable<T> list1, IEnumerable<T> list2)
		{
			IEnumerator<T>[] enumerators = new[] { list1.GetEnumerator(), list2.GetEnumerator() };
			while (true)
			{
				int current = 0;
				T[] result = new T[enumerators.Length];
				foreach (IEnumerator<T> enumerator in enumerators)
				{
					if (!enumerator.MoveNext())
					{
						yield break;
					}
					result[current++] = enumerator.Current;
				}
				yield return result;
			}
		}

		public static void WriteToConsole<T>(this IEnumerable<T> data, string label)
		{
			string result = StringFormatter.FormatData(",", data);
			Console.WriteLine(label + result);
		}

		public static void IterateIndex<T>(this T[] items, Action<int, T> action)
		{
			if (items == null)
			{
				throw new ArgumentNullException("items");
			}
			if (action == null)
			{
				throw new ArgumentNullException("action");
			}

			int lower = items.GetLowerBound(0);
			int upper = items.GetUpperBound(0);
			for (int idx = lower; idx < upper; idx++)
			{
				action(idx, items[idx]);
			}
		}

		public static void IterateIndex<T>(this IList<T> items, Action<int, T> action)
		{
			if (items == null)
			{
				throw new ArgumentNullException("items");
			}
			if (action == null)
			{
				throw new ArgumentNullException("action");
			}

			for (int idx = 0; idx < items.Count; idx++)
			{
				action(idx, items[idx]);
			}
		}

		/// <summary>
		/// Iterates backwards to assist in removing items from the list.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="items"></param>
		/// <param name="action"></param>
		public static void ForEachBackwards<T>(this IList<T> items, Action<T> action)
		{
			if (items == null)
			{
				throw new ArgumentNullException("items");
			}
			if (action == null)
			{
				throw new ArgumentNullException("action");
			}

			for (int i = items.Count - 1; i > -1; --i)
			{
				action(items[i]);
			}
		}

		/// <summary>
		/// Iterates backwards to assist in removing items from the list.  This Action takes an additional parameter.
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <param name="items"></param>
		/// <param name="action"></param>
		/// <param name="arg2"></param>
		public static void ForEachBackwards<T1, T2>(this IList<T1> items, Action<T1, T2> action, T2 arg2)
		{
			if (items == null)
			{
				throw new ArgumentNullException("items");
			}
			if (action == null)
			{
				throw new ArgumentNullException("action");
			}

			for (int i = items.Count - 1; i > -1; --i)
			{
				action(items[i], arg2);
			}
		}

		// Searches a list for a given element using a binary search algorithm.
		// Elements of the array are compared to the search value using provided
		// comparer on the elements of the list and the given search value.
		// This method assumes that the list is already sorted according to the
		// comparer function; if this is not the case, the result will be
		// incorrect.
		//
		// The method returns the index of the given value in the list. If the
		// list does not contain the given value, the method returns a negative
		// integer. The bitwise complement operator (~) can be applied to a
		// negative result to produce the index of the first element (if any) that
		// is larger than the given search value.
		public static Int32 BinarySearch<T1, T2>(this IList<T1> list, T2 value, Func<T2, T1, Int32> compare)
		{
			Int32 low = 0;
			Int32 high = list.Count - 1;
			while (low <= high)
			{
				Int32 middle = low + ((high - low) / 2);
				Int32 compValue = compare(value, list[middle]);

				if (compValue == 0)
				{
					return middle;
				}
				else if (compValue < 0)
				{
					high = middle - 1;
				}
				else
				{
					low = middle + 1;
				}
			}

			return ~low;
		}

		public static Int32 BinarySearch<T1>(this IList<T1> list, T1 value, IComparer<T1> comparer)
		{
			return BinarySearch(list, value, comparer.Compare);
		}

		public static Int32 BinarySearch<T1>(this IList<T1> list, T1 value)
		{
			return BinarySearch(list, value, Comparer<T1>.Default);
		}

		#region ForEach

		public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
		{
			if (source.IsNull())
			{
				return;
			}
			foreach (T item in source)
			{
				action(item);
			}
		}

		public static void ForEach<T>(this ICollection<T> source, Action<T> action)
		{
			if (source.IsNull())
			{
				return;
			}
			foreach (T item in source)
			{
				action(item);
			}
		}

		public static void ParallelForEach<T>(this IEnumerable<T> source, Action<T> action)
		{
			Parallel.ForEach(source, action);
		}

		public static void ForEach<T>(this IList<T> source, Action<T> action)
		{
			if (source.IsNull())
			{
				return;
			}
			for (int i = 0; i < source.Count; i++)
			{
				action(source[i]);
			}
		}

		public static void ParallelForEach<T>(this IList<T> source, Action<T> action)
		{
			Parallel.ForEach(source, action);
		}

		public static void ForEach<T>(this IEnumerable source, Action<T> action)
		{
			if (source.IsNull())
			{
				return;
			}
			foreach (T item in source)
			{
				action(item);
			}
		}

		public static void ForEach<T>(this IList source, Action<T> action)
		{
			if (source.IsNull())
			{
				return;
			}
			foreach (T item in source)
			{
				action(item);
			}
		}

		#endregion ForEach
	}
}